package com.ztmao.shortlink.project.service.Impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ztmao.shortlink.project.common.constant.RedisCacheConstant;
import com.ztmao.shortlink.project.common.convention.exception.ClientException;
import com.ztmao.shortlink.project.common.convention.exception.ServiceException;
import com.ztmao.shortlink.project.common.enums.VailDateTypeEnum;
import com.ztmao.shortlink.project.config.GoToDomainConfiguration;
import com.ztmao.shortlink.project.dao.entity.*;
import com.ztmao.shortlink.project.dao.mapper.*;
import com.ztmao.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import com.ztmao.shortlink.project.dto.req.ShortLinkBatchCreateReqDTO;
import com.ztmao.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.ztmao.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.ztmao.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.ztmao.shortlink.project.dto.resp.*;
import com.ztmao.shortlink.project.mq.producer.DelayShortLinkStatsProducer;
import com.ztmao.shortlink.project.mq.producer.ShortLinkStatsSaveProducer;
import com.ztmao.shortlink.project.service.ShortLinkService;
import com.ztmao.shortlink.project.toolkit.HashUtil;
import com.ztmao.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.lang.model.type.ReferenceType;
import javax.sql.rowset.serial.SerialException;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.ztmao.shortlink.project.common.constant.MQConstant.TOPIC_SHORT_LINK_STATS;
import static com.ztmao.shortlink.project.common.constant.RedisCacheConstant.*;
import static com.ztmao.shortlink.project.common.constant.ShortLinkConstant.AMAP_REMOTE_URL;

/**
 *短链接服务实现类
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {
    private final RBloomFilter<String> shortLinkCreateCachePenetrationBloomFilter;
    private final StringRedisTemplate stringRedisTemplate;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final RedissonClient redissonClient;
    private final GoToDomainConfiguration goToDomainConfiguration;
    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer;
    private final  RocketMQTemplate rocketMQTemplate;
    @Value("${short-link.stats.locale.amap-key}")
    private String statsLocaleAmapKey;

    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;

    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO reqDTO) {
        //验证白名单
        verificationWhitelist(reqDTO.getOriginUrl());

        String shortUri = createSuffix(reqDTO.getOriginUrl(), createShortLinkDefaultDomain);

        String fullShortUrl = createShortLinkDefaultDomain + "/" + shortUri;
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(createShortLinkDefaultDomain)
                .originUrl(reqDTO.getOriginUrl())
                .gid(reqDTO.getGid())
                .createdType(reqDTO.getCreatedType())
                .validDateType(reqDTO.getValidDateType())
                .validDate(reqDTO.getValidDate())
                .description(reqDTO.getDescription())
                .shortUri(shortUri)
                .enableStatus(0)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .fullShortUrl(fullShortUrl)
//                .favicon(getFavicon(requestParam.getOriginUrl()))
                .build();
        //同时添加路由表
        ShortLinkGotoDO shortLinkGotoDo = ShortLinkGotoDO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(reqDTO.getGid()).build();

        try{
            baseMapper.insert(shortLinkDO);
            shortLinkGotoMapper.insert(shortLinkGotoDo);
        }catch (DuplicateKeyException ex){
            throw new ServiceException("创建短链接失败，重复的短链接");
        }
        //加入缓存
        shortLinkCreateCachePenetrationBloomFilter.add(fullShortUrl);
        //缓存预热
        stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl),
                shortLinkDO.getOriginUrl(),
                LinkUtil.getLinkCacheValidateTime(shortLinkDO.getValidDate()),
                TimeUnit.MILLISECONDS);

        ShortLinkCreateRespDTO shortLinkCreateRespDTO = ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://"+fullShortUrl)
                .gid(reqDTO.getGid())
                .originUrl(reqDTO.getOriginUrl()).build();
        return shortLinkCreateRespDTO;
    }
    @Override
    public ShortLinkCreateRespDTO createShortLinkByLock(ShortLinkCreateReqDTO requestParam) {
        verificationWhitelist(requestParam.getOriginUrl());
        String fullShortUrl;
        // 为什么说布隆过滤器性能远胜于分布式锁？详情查看：https://nageoffer.com/shortlink/question
        RLock lock = redissonClient.getLock(SHORT_LINK_CREATE_LOCK_KEY);
        lock.lock();
        try {
            String shortLinkSuffix = generateSuffixByLock(requestParam);
            fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                    .append("/")
                    .append(shortLinkSuffix)
                    .toString();
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(createShortLinkDefaultDomain)
                    .originUrl(requestParam.getOriginUrl())
                    .gid(requestParam.getGid())
                    .createdType(requestParam.getCreatedType())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .description(requestParam.getDescription())
                    .shortUri(shortLinkSuffix)
                    .enableStatus(0)
                    .totalPv(0)
                    .totalUv(0)
                    .totalUip(0)
                    .delTime(0L)
                    .fullShortUrl(fullShortUrl)
//                    .favicon(getFavicon(requestParam.getOriginUrl()))
                    .build();
            ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .gid(requestParam.getGid())
                    .build();
            try {
                baseMapper.insert(shortLinkDO);
                shortLinkGotoMapper.insert(linkGotoDO);
            } catch (DuplicateKeyException ex) {
                throw new ServiceException(String.format("短链接：%s 生成重复", fullShortUrl));
            }
            stringRedisTemplate.opsForValue().set(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    requestParam.getOriginUrl(),
                    LinkUtil.getLinkCacheValidateTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS
            );
        } finally {
            lock.unlock();
        }
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + fullShortUrl)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }
    private void verificationWhitelist(String originUrl) {
        //判断是否开启白名单
        Boolean enabled = goToDomainConfiguration.getEnabled();
        if( enabled == null || enabled != true ){
            return;
        }

        String domain = LinkUtil.getActualDomain(originUrl);
        if(StrUtil.isBlank(domain)){
            throw new ClientException("请输入正确的链接");
        }
        //判断是否在白名单中
        List<String> whiteList = goToDomainConfiguration.getDetails();
        if (!whiteList.contains(domain)){
            throw new ClientException("演示环境为避免恶意攻击，请生成以下网站跳转链接:"+goToDomainConfiguration.getNames());
        }
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
//        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getGid, requestParam.getGid())
//                .eq(ShortLinkDO::getEnableStatus, 0)
//                .eq(ShortLinkDO::getDelFlag, 0)
//                .orderByDesc(ShortLinkDO::getCreateTime);
//        IPage<ShortLinkDO> resultPage = baseMapper.selectPage(requestParam,queryWrapper);
        IPage<ShortLinkDO> resultPage = baseMapper.pageLink(requestParam);
        return resultPage.convert(each -> {
            ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            result.setDomain("http://" + result.getDomain());
            return result;
        });
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        QueryWrapper<ShortLinkDO> wrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid, count(*) as shortLinkCount")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .eq("del_flag", 0)

                .groupBy("gid");
        List<Map<String, Object>> shortLinkCountList = baseMapper.selectMaps(wrapper);
        return BeanUtil.copyToList(shortLinkCountList, ShortLinkGroupCountQueryRespDTO.class);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getOriginGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        if (hasShortLinkDO == null) {
            throw new ClientException("短链接记录不存在");
        }
        //未修改分组直接修改短链接表数据
        if (Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid())) {
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(hasShortLinkDO.getDomain())
                    .shortUri(hasShortLinkDO.getShortUri())
                    .favicon(hasShortLinkDO.getFavicon())
                    .createdType(hasShortLinkDO.getCreatedType())
                    .gid(requestParam.getGid())
                    .originUrl(requestParam.getOriginUrl())
                    .description(requestParam.getDescribe())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .build();
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()), ShortLinkDO::getValidDate, null);
            baseMapper.update(shortLinkDO, updateWrapper);
        } else {
            //获得分布式写锁
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, hasShortLinkDO.getGid()));
            RLock rLock = readWriteLock.writeLock();
            rLock.lock();
            try {
                //将原有短链接逻辑删除，
                LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0);
                ShortLinkDO delShortLink = ShortLinkDO.builder()
                        .delTime(System.currentTimeMillis())
                        .build();
                delShortLink.setDelFlag(1);
                baseMapper.update(delShortLink, updateWrapper);
                //插入更新的短链接
                ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                        .domain(createShortLinkDefaultDomain)
                        .originUrl(requestParam.getOriginUrl())
                        .gid(requestParam.getGid())
                        .createdType(hasShortLinkDO.getCreatedType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate(requestParam.getValidDate())
                        .description(requestParam.getDescribe())
                        .shortUri(hasShortLinkDO.getShortUri())
                        .enableStatus(hasShortLinkDO.getEnableStatus())
                        .totalPv(hasShortLinkDO.getTotalPv())
                        .totalUv(hasShortLinkDO.getTotalUv())
                        .totalUip(hasShortLinkDO.getTotalUip())
                        .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                        .favicon(getFavicon(requestParam.getOriginUrl()))
                        .delTime(0L)
                        .build();
                baseMapper.insert(shortLinkDO);
                //删除路由表
                LambdaQueryWrapper<ShortLinkGotoDO> eq = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, hasShortLinkDO.getFullShortUrl())
                        .eq(ShortLinkGotoDO::getGid, hasShortLinkDO.getGid());
                shortLinkGotoMapper.delete(eq);
                //插入路由表
                ShortLinkGotoDO shortLinkGotoDo = ShortLinkGotoDO.builder()
                        .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                        .gid(requestParam.getGid())
                        .build();
                shortLinkGotoMapper.insert(shortLinkGotoDo);

            }finally {
                rLock.unlock();
            }
        }
        //修改缓存
        if(!Objects.equals(hasShortLinkDO.getValidDateType(), requestParam.getValidDateType())
                || !Objects.equals(hasShortLinkDO.getValidDate(),requestParam.getValidDate())
                || !Objects.equals(hasShortLinkDO.getOriginUrl(), requestParam.getOriginUrl())){
            stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, hasShortLinkDO.getFullShortUrl()));

            if(hasShortLinkDO.getValidDate() != null && hasShortLinkDO.getValidDate().before(new Date()) ){
                if(requestParam.getValidDateType() == VailDateTypeEnum.PERMANENT.getType() || requestParam.getValidDate().after(new Date())){
                    stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, hasShortLinkDO.getFullShortUrl()));
                }
        }
        }
    }

    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) throws IOException {
        String serverName = request.getServerName();
        String fullShortUrl = serverName + ":8001/" + shortUri;
        //查询缓存是否存在
        String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        if(!StrUtil.isBlank(originalLink)){
            ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(shortLinkStatsRecordDTO);
            ((HttpServletResponse)response).sendRedirect(originalLink);
            return;
        }
        //加一层布隆过滤器的判断
        boolean contains = shortLinkCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        if (!contains) {
            ((HttpServletResponse)response).sendRedirect("/page/notfound");
            return;
        }
        //对缓存空值的判断
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(gotoIsNullShortLink)){
            ((HttpServletResponse)response).sendRedirect("/page/notfound");
            return;
        }
            //缓存不存在,获取分布式锁再进行数据库查询
            String lockKey = String.format(RedisCacheConstant.LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl);

            RLock lock = redissonClient.getLock(lockKey);
            lock.lock();
            try {
                //再次查询缓存是否存在(防止等待锁 的请求获取锁后再次请求数据库)
                originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
                if (!StrUtil.isBlank(originalLink)) {
                    ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
                    shortLinkStats(shortLinkStatsRecordDTO);
                    ((HttpServletResponse) response).sendRedirect(originalLink);
                    return;
                }
                //缓存不存在,查询数据库
                LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
                if (shortLinkGotoDO == null) {
                    //缓存空值
                    stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                    ((HttpServletResponse)response).sendRedirect("/page/notfound");
                    return;
                }
                LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                        .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                        .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getEnableStatus, 0);

                ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
                if (shortLinkDO == null|| shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date())) {

                    stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                    ((HttpServletResponse) response).sendRedirect("/page/notfound");
                    return;
                }

                    stringRedisTemplate.opsForValue().set(
                            String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                            shortLinkDO.getOriginUrl(),
                            LinkUtil.getLinkCacheValidateTime(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS
                    );
                ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
                shortLinkStats(shortLinkStatsRecordDTO);
                    ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());

            }finally {
                lock.unlock();
            }
    }

    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        // 分别获取短链接和对应描述的list
        List<String> originUrlList = requestParam.getOriginUrls();
        List<String> descriptionList = requestParam.getDescribes();
        List<ShortLinkBaseInfoRespDTO> baseLinkInfos = new ArrayList<>();
        //遍历list创建短链接
        for(int i=0 ; i<originUrlList.size() ; i++){
            String originUrl = originUrlList.get(i);
            String description = descriptionList.get(i);
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrl);
            shortLinkCreateReqDTO.setDescription(description);
            try {
                ShortLinkCreateRespDTO shortLink = createShortLink(shortLinkCreateReqDTO);
                ShortLinkBaseInfoRespDTO baseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .describe(description)
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .originUrl(originUrl)
                        .build();
                baseLinkInfos.add(baseInfoRespDTO);

            }catch (Exception ex){
                        log.error("批量创建短链接失败",ex);
            }

        }
        ShortLinkBatchCreateRespDTO result = ShortLinkBatchCreateRespDTO.builder()
                .baseLinkInfos(baseLinkInfos)
                .total(baseLinkInfos.size())
                .build();
        return result;
    }

    private String generateSuffixByLock(ShortLinkCreateReqDTO requestParam) {
        int customGenerateCount = 0;
        String shorUri;
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            String originUrl = requestParam.getOriginUrl();
            originUrl += cn.hutool.core.lang.UUID.randomUUID().toString();
            // 短链接哈希算法生成冲突问题如何解决？详情查看：https://nageoffer.com/shortlink/question
            shorUri = HashUtil.hashToBase62(originUrl);
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, createShortLinkDefaultDomain + "/" + shorUri)
                    .eq(ShortLinkDO::getDelFlag, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
            if (shortLinkDO == null) {
                break;
            }
            customGenerateCount++;
        }
        return shorUri;
    }

    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, ServletRequest request, ServletResponse response) {
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        AtomicReference<String> uv = new AtomicReference<>();
        Runnable addResponseCookieTask = () -> {
            uv.set(cn.hutool.core.lang.UUID.fastUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            ((HttpServletResponse) response).addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);
            stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, uv.get());
        };
        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        uv.set(each);
                        Long uvAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, each);
                        uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                    }, addResponseCookieTask);
        } else {
            addResponseCookieTask.run();
        }
        String remoteAddr = LinkUtil.getActualIp(((HttpServletRequest) request));
        String os = LinkUtil.getOs(((HttpServletRequest) request));
        String browser = LinkUtil.getBrowser(((HttpServletRequest) request));
        String device = LinkUtil.getDevice(((HttpServletRequest) request));
        String network = LinkUtil.getNetwork(((HttpServletRequest) request));
        Long uipAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uip:" + fullShortUrl, remoteAddr);
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .build();
    }


    @Override
    public void shortLinkStats(ShortLinkStatsRecordDTO statsRecord) {

        rocketMQTemplate.asyncSend(TOPIC_SHORT_LINK_STATS,statsRecord,new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("发送短链接访问量统计消息成功");
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("发送短链接访问量统计消息失败",throwable);
            }
        });
    }


    @Override
    public void actualShortLinkStats(ShortLinkStatsRecordDTO shortLinkStatsRecord) {
        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("statsRecord", JSON.toJSONString(shortLinkStatsRecord));
        shortLinkStatsSaveProducer.send(hashMap);
    }



    private String createSuffix(String originalUrl,String domain) {

        String shortUri =null;

        int retryCount=0;
        while(true){
            //在原始链接后添加uuid再用于生成短链接，来降低冲突概率
            originalUrl+= UUID.randomUUID().toString();
            shortUri = HashUtil.hashToBase62(originalUrl);
            if(retryCount > 10){
                throw new ServiceException("生成短链接重复");
            }
            if(!shortLinkCreateCachePenetrationBloomFilter.contains(domain+"/"+shortUri)){
                break;
            }
            retryCount++;
        }
        return shortUri;
    }
    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (HttpURLConnection.HTTP_OK == responseCode) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }
}
